﻿/* 
 * 
 * Copyright 2012 Bendsoft. All rights reserved.
 * http://www.bendsoft.com
 * 
 * This code is distributed under the New BSD License (BSD).
 * 
 * @package     Camelot SharePoint Integration Library
 * @codeplex    http://camelottoolkit.codeplex.com
 * @license     New BSD License (BSD)
 * 
 */

using Camelot.SharePointConnector.Data;
using Camelot.SharePointIntegration.Client;
using System;
using System.Diagnostics;
using System.Security.Principal;
using System.ServiceModel;

namespace Camelot.SharePointIntegration.Wcf
{
    public class CamelotService : ICamelotService
    {
        private EventLog eventLog;
        private const string EventSource = "SharePointIntegrationService";

        public CamelotService()
        {
            // Load license file locally if exists
            string executionDirectory = System.IO.Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;
            if (System.IO.File.Exists(System.IO.Path.Combine(executionDirectory, "license_signed.xml")))
            {
                SharePointConnection.ProvideLicenseFile(System.IO.Path.Combine(executionDirectory, "license_signed.xml"));
            }

            if (System.Configuration.ConfigurationManager.AppSettings["SHARED_KEY"] == null)
                throw new Exception("'SHARED_KEY' is not set in application settings");

            if (System.Configuration.ConfigurationManager.AppSettings["WRITE_CDATA"] != null)
                bool.TryParse(System.Configuration.ConfigurationManager.AppSettings["WRITE_CDATA"], out writeCData);

            if (System.Configuration.ConfigurationManager.AppSettings["ENABLE_IMPERSONATION"] != null)
                bool.TryParse(System.Configuration.ConfigurationManager.AppSettings["ENABLE_IMPERSONATION"], out enableImpersonation);

            appSharedKey = System.Configuration.ConfigurationManager.AppSettings["SHARED_KEY"].MD5();

            if (!EventLog.SourceExists(EventSource))
                EventLog.CreateEventSource(EventSource, "Camelot");

            this.eventLog = new EventLog() { Source = EventSource, Log = "Camelot" };
        }

        /// <summary>
        /// Private md5 encrypted key
        /// </summary>
        private string appSharedKey;

        /// <summary>
        /// Indicates whether CDATA brackets are used in CamelotXml
        /// </summary>
        private bool writeCData;

        /// <summary>
        /// Indicates whether this service can impersonate the client when querying SharePoint
        /// </summary>
        private bool enableImpersonation = true;

        /// <summary>
        /// Returns a Camelot Xml from specified List and View
        /// </summary>
        /// <param name="listName"></param>
        /// <param name="viewName"></param>
        /// <param name="includeAttachments"></param>
        /// <param name="connectionString"></param>
        /// <param name="compression"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public string GetListCamelotXml(string listName, string viewName, bool includeAttachments, string connectionString, bool compression, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = securityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        var listData = ListDataUtility.CreateListData(listName, viewName, includeAttachments, connection);
                        var camelotXml = CamelotXml.CreateXml(listData, writeCData);

                        if (compression == true)
                        {
                            return Convert.ToBase64String(camelotXml.DeflateCompress());
                        }
                        else
                        {
                            return camelotXml;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Returns a Camelot Xml from SQL query
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="connectionString"></param>
        /// <param name="compression"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public string ExecuteCamelotXml(string sql, string connectionString, bool compression, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                {
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();
                }

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        ListData listData = ListDataUtility.CreateListData(sql, connection);


                        var camelotXml = CamelotXml.CreateXml(listData, writeCData);

                        if (compression == true)
                        {
                            return Convert.ToBase64String(camelotXml.DeflateCompress());
                        }
                        else
                        {
                            return camelotXml;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Returns a Camelot Xml from SQL query
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="connectionString"></param>
        /// <param name="compression"></param>
        /// <param name="sharedKey"></param>
        /// <param name="skip"></param>
        /// <param name="take"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        [Obsolete("Use LIMIT with the ExecuteCamelotXml method instead")]
        public string ExecuteCamelotXml(string sql, string connectionString, bool compression, string sharedKey, int skip, int take)
        {
            if (skip == 0 && take == 0)
                return ExecuteCamelotXml(sql, connectionString, compression, sharedKey);

            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        ListData listData = ListDataUtility.CreateListData(sql, connection, skip, take);

                        var camelotXml = CamelotXml.CreateXml(listData, writeCData);

                        if (compression == true)
                        {
                            return Convert.ToBase64String(camelotXml.DeflateCompress());
                        }
                        else
                        {
                            return camelotXml;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Return the number of rows returned from a query
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <example>ExecuteCount("SELECT ID FROM Tasks WHERE ID > 10", "sharepoint_connection", "813229844ee022045bf16ae72f7619fd")</example>
        /// <returns></returns>
        [Obsolete("Use Count(ID) in the SQL instead")]
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public int ExecuteCount(string sql, string connectionString, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    var dt = Camelot.SharePointConnector.Data.Helper.ExecuteDataTable(sql, System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString);
                    return dt.Rows.Count;
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Executes the query and returns the first column of the first row in the result set. Additional columns and rows are ignored.
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public string ExecuteScalar(string sql, string connectionString, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        using (SharePointCommand command = new SharePointCommand(sql, connection))
                        {
                            var returnValue = command.ExecuteScalar();
                            if (returnValue != null)
                            {
                                return returnValue.ToString();
                            }
                            else
                            {
                                return command.LastInsertedId.ToString();
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Executes the query without returning anything
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public bool ExecuteNonQuery(string sql, string connectionString, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        using (SharePointCommand command = new SharePointCommand(sql, connection))
                        {
                            command.ExecuteNonQuery();
                            return true;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Uploads a file to a document library
        /// </summary>
        /// <param name="listName"></param>
        /// <param name="file"></param>
        /// <param name="data"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public bool UploadFile(string listName, string file, byte[] data, string connectionString, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        using (SharePointCommand command = new SharePointCommand(string.Format("CALL UPLOAD('{0}', '{1}', @data)", listName, file), connection))
                        {
                            command.Parameters.Add("@data", data);
                            command.ExecuteNonQuery();
                            return true;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Uploads an attachment to an item
        /// </summary>
        /// <param name="listName"></param>
        /// <param name="itemId"></param>
        /// <param name="fileName"></param>
        /// <param name="data"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public bool UploadAttachment(string listName, int itemId, string fileName, byte[] data, string connectionString, string sharedKey)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();
                        using (SharePointCommand command = new SharePointCommand(string.Format("CALL UPLOADATTACHMENT('{0}', {1}, '{2}', @data)", listName, itemId, fileName), connection))
                        {
                            command.Parameters.Add("@data", data);
                            command.ExecuteNonQuery();
                            return true;
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Downloads a file from a document library
        /// </summary>
        /// <param name="listName"></param>
        /// <param name="file"></param>
        /// <param name="connectionString"></param>
        /// <param name="sharedKey"></param>
        /// <param name="version"></param>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public byte[] DownloadFile(string listName, string file, string connectionString, string sharedKey, double? version)
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                // Simple authentication check, replace with own method if stronger security is needed
                if (appSharedKey != sharedKey)
                    throw new FaultException("Authentication failed", new FaultCode("AccessDenied"));

                if (System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString] == null)
                    throw new FaultException("The connection string was not found", new FaultCode("UnknownConnectionString"));

                try
                {
                    using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings[connectionString].ConnectionString))
                    {
                        connection.Open();

                        // Add version if any
                        string versionData = version == null ? string.Empty : string.Concat(", ", ((double)version).ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));

                        using (SharePointCommand command = new SharePointCommand(string.Format("CALL DOWNLOAD('{0}', '{1}'{2})", listName, file, versionData), connection))
                        {
                            return (byte[])command.ExecuteScalar();
                        }
                    }
                }
                catch (Exception e)
                {
                    WriteEventLogError(e);
                    throw new FaultException(e.Message, new FaultCode(e.GetType().Name));
                };
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Returns the local server time
        /// </summary>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public string GetServerTime()
        {
            return System.DateTime.Now.ToString();
        }

        /// <summary>
        /// Returns the name of the current user
        /// </summary>
        /// <returns></returns>
        [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
        public string GetUser()
        {
            WindowsImpersonationContext context = null;
            ServiceSecurityContext securityContext = OperationContext.Current.ServiceSecurityContext;

            try
            {
                if (enableImpersonation && securityContext != null && securityContext.WindowsIdentity != null &&
                    (securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Impersonation || securityContext.WindowsIdentity.ImpersonationLevel == TokenImpersonationLevel.Delegation))
                    context = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate();

                return WindowsIdentity.GetCurrent().Name;
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }
        }

        /// <summary>
        /// Write errors to the event log
        /// </summary>
        /// <param name="e"></param>
        private void WriteEventLogError(Exception e)
        {
            eventLog.WriteEntry(
                string.Concat(
                    "Exception message and trace", Environment.NewLine,
                    e.Message, Environment.NewLine,
                    e.StackTrace, Environment.NewLine, Environment.NewLine,
                    "-----------------------------------------------------------------------------------", Environment.NewLine,
                    "InnerException message and trace", Environment.NewLine,
                    e.InnerException.Message, Environment.NewLine,
                    e.InnerException.StackTrace
                    ),
                    EventLogEntryType.Error
                );
        }
    }
}
